我们客户端和服务器进行通信,往往服务端响应给我们的数据是一个 Json 串,在 Flutter 中并没有像 Java 中那样的 Gson、fasejson 之类的第三方库。
Flutter 提供了两种常规的 JSON 使用策略:
- 手动序列化
- 利用代码进行自动序列化
手动序列化
手动的 JSON 解码是指使用 dart:convert
中内置的 JSON 解码器。它包括将原始 JSON 字符串传递给 jsonDecode()
方法,然后在产生的 Map
计算结果中寻找你需要的值。
Dio dio = Dio();
Response response = await dio.getUri(
Uri.parse("https://jsonplaceholder.typicode.com/albums/1"),
options: Options(
responseType: ResponseType.plain,
));
String jsonString = response.data.toString();
print(jsonString);
Map<String, dynamic> obj = jsonDecode(jsonString);
print('userId = ${obj['userId']}');
print('id = ${obj['id']}');
print('title = ${obj['title']}');
}
上面的代码输出:
I/flutter (29226): {
I/flutter (29226): "userId": 1,
I/flutter (29226): "id": 1,
I/flutter (29226): "title": "quidem molestiae enim"
I/flutter (29226): }
I/flutter (29226): userId = 1
I/flutter (29226): id = 1
I/flutter (29226): title = quidem molestiae enim
在上面的例子中,jsonDecode()
返回一个 Map
,这意味着你在运行时以前都不知道值的类型。使用这个方法,你失去了大部分的静态类型语言特性:类型安全,自动补全以及最重要的编译时异常。你的代码会立即变得更加容易出错。
所以我们最好是在模型类中序列化 JSON 数据:
将上面的 JSON 抽象为一个模型:
class Music {
int userId;
int id;
String title;
Music(this.userId, this.id, this.title);
Music.fromJson(Map<String, dynamic> json)
: userId = json['userId'],
id = json['id'],
title = json['title'];
}
在上面的代码中,一个 Music.fromJson()
构造函数,用于从 map 结构中构造一个新的 Music
实例;解码逻辑的责任现在移动到了模型内部。通过这个新方法,你可以很容易地解码一个 music,通过这种方法,调用代码可以拥有类型安全,字段的自动完成以及编译时异常(检测)。如果你发生了笔误或者把 String
类型的字段看成了 int
类型, app 将不会编译,而不是在运行时崩溃。
如果要将一个对象序列化为一个 Json 字符串,需要修改一下上面的模型,添加一个方法:
Map<String, dynamic> toJson() =>
{
'userId': userId,
'id': id,
'title': title,
};
这样,直接调用 jsonEncode
即可:
Music music = Music(2, 2, "月亮之上");
String json = jsonEncode(music);
print(json);
输出如下:
I/flutter (29226): {"userId":2,"id":2,"title":"月亮之上"}
以上方法只适合一些简单的、数据量小的 Json 处理,当有复杂的、以及对象嵌套的 JSON 需要处理的时候,这种方法就不合适了,需要:
使用代码生成库序列化 JSON 数据
尽管有其它库可以使用,本指南使用了json_serializable
,一个自动化源代码生成器来为你生成 JSON 序列化数据模板。
由于序列化数据代码不再需要手动编写或者维护,你可以将序列化 JSON 数据在运行时的异常风险降到最低。
在项目中设置 json_serializable
要在你的项目中包含 json_serializable
,你需要一个常规依赖,以及两个 dev 依赖。简单来说,dev 依赖 是不包括在我们的 App 源代码中的依赖 - 它们只会被用在开发环境中。
在序列化 JSON 数据的例子中,这些必须的依赖的最新版本可以在下面 [pubspec 文件][the pubspec file] 中查看。
pubspec.yaml
dependencies:
# Your other regular dependencies here
json_annotation: <latest_version>
dev_dependencies:
# Your other dev_dependencies here
build_runner: <latest_version>
json_serializable: <latest_version>
在你的项目根文件夹下运行 flutter pub get
(或者在你的编辑器中点击 Packages Get)以确保在你的项目中可以使用这些新的依赖。
以 json_serializable 的方式创建模型类
下面显示了怎样将 User
类转换为 json_serializable
后的类。简单起见,该代码使用了前面的例子中的简化的 JSON 模型。
user.dart
import 'package:json_annotation/json_annotation.dart';
part 'User.g.dart';
@JsonSerializable()
class User {
User(this.name, this.email);
String name;
String email;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
通过这个设置,源代码生成器将生成用于 JSON 编码及解码 name
以及 email
字段的代码。
如果需要,你可以很容易自定义命名策略。例如,如果 API 返回带有蛇形命名方式的对象,并且你想要在你的模型里使用 小驼峰 的命名方式,你可以使用带有一个 name 参数的 @JsonKey
注解。
@JsonKey(name: 'registration_date_millis')
final int registrationDateMillis;
最好是服务器和客户端都遵循相同的命名策略。
@JsonSerializable()
提供 fieldRename
枚举,以将 dart 字段完全转换为 JSON 密钥。
修改 @JsonSerializable(fieldRename:FieldRename.snake)
等同于向每个字段添加@JsonKey(name:'<snake_case>')
。
有时服务器数据不确定,因此有必要验证和保护客户端上的数据。
其他常用的 @JsonKey
注释包括:
/// 如果JSON不包含此键或值是'null',则告诉json_serializable使用“ defaultValue”。
@JsonKey(defaultValue: false)
final bool isAdult;
/// 当`true`时告诉json_serializable JSON必须包含密钥时,如果密钥不存在,则会引发异常。
@JsonKey(required: true)
final String id;
/// 当`true`告诉json_serializable时,生成的代码应该完全忽略该字段。
@JsonKey(ignore: true)
final String verificationCode;
运行代码生成工具
当你首次创建 json_serializable
类时,你会得到类似下图的错误。
这些错误完全正常,很简单,因为这些模型类的生成代码并不存在。要解决这个问题,运行代码生成器来生成序列化数据模板。
有两种方式运行代码生成器。
一次性代码生成
通过在项目根目录运行 flutter pub run build_runner build
,你可以在任何需要的时候为你的模型生成 JSON 序列化数据代码。这会触发一次构建,遍历源文件,选择相关的文件,然后为它们生成必须的序列化数据代码。
虽然这样很方便,但是如果你不需要在每次修改了你的模型类后都要手动构建那将会很棒。
持续生成代码
监听器 让我们的源代码生成过程更加方便。它监听我们项目中的文件变化并且会在需要的时候自动构建必要的文件。通过在项目根目录运行 flutter pub run build_runner watch
启动监听。
一旦启动监听并让它留在后台运行是安全的。
使用 json_serializable 模型
为了以 json_serializable
的方式解码 JSON 字符串,事实上你不必对以前的代码做任何的改动。
Map userMap = jsonDecode(jsonString);
var user = User.fromJson(userMap);
编码也是如此。调用 API 和以前一样。
String json = jsonEncode(user);
使用 json_serializable
,在 User
类中你可以忘记手动序列化任意的 JSON 数据。源代码生成器会创建一个名为 user.g.dart
的文件,它包含了所有必须的序列化数据逻辑。你不必再编写自动化测试来确保序列化数据奏效。现在由 库来负责*确保序列化数据能正确地奏效。
为嵌套类 (Nested Classes) 生成代码
你可能类在代码中用了嵌套类,在你把类作为参数传递给一些服务(比如 Firebase)的时候,你可能会遇到Invalid argument
错误。
比如下面的这个 Address
类:
import 'package:json_annotation/json_annotation.dart';
part 'Address.g.dart';
@JsonSerializable()
class Address {
String street;
String city;
Address(this.street, this.city);
factory Address.fromJson(Map<String, dynamic> json) => _$AddressFromJson(json);
Map<String, dynamic> toJson() => _$AddressToJson(this);
}
一个 Address
类被嵌套在 User
类中使用:
import 'address.dart';
import 'package:json_annotation/json_annotation.dart';
part 'User.g.dart';
@JsonSerializable()
class User {
String firstName;
Address address;
User(this.firstName, this.address);
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
在终端中运行 flutter pub run build_runner build
创建 * .g.dart
文件,但私有函数如 _ $ UserToJson()
会看起来像下面这样:
(
Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
'firstName': instance.firstName,
'address': instance.address,
};
看起来没有什么问题,如果 print
用户对象时:
Address address = Address("My st.", "New York");
User user = User("John", address);
print(user.toJson());
结果会是:
{name: John, address: Instance of 'address'}
但实际上你希望的输出结果是这样的:
{name: John, address: {street: My st., city: New York}}
为了得到正常的输出,你需要在类声明之前为 @JsonSerializable
方法加入 explicitToJson: true
参数, User
类现在看起来是这样的:
import 'address.dart';
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';
@JsonSerializable(explicitToJson: true)
class User {
String firstName;
Address address;
User(this.firstName, this.address);
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
了解更多信息,请查阅 json_annotation
这个 package 里的 JsonSerializable
类的 explicitToJson
参数等相关文档。